using System;
using System.Diagnostics;
using System.Collections.Generic;

namespace OpenRay
{
	public class BoundingBox
	{
		private readonly Point min, max;
		
		public BoundingBox ( Point p )
		{
			min = p;
			max = p;
		}
		
		public BoundingBox (Point min, Point max)
		{
			Debug.Assert( min.X <= max.X, "Max is below min." );
			Debug.Assert( min.Y <= max.Y, "Max is below min." );
			Debug.Assert( min.Z <= max.Z, "Max is below min." );
			
			this.min = min;
			this.max = max;
		}
		
		public bool Overlaps( BoundingBox b2 )
		{
			return ( b2.min.X <= max.X && b2.max.X >= min.X ) &&
            	   ( b2.min.Y <= max.Y && b2.max.Y >= min.Y ) &&
            	   ( b2.min.Z <= max.Z && b2.max.Z >= min.Z );
		}
		
		public bool IsInside( Point p )
		{
			 return ( min.X <= p.X && p.X <= max.X &&
             		  min.Y <= p.Y && p.Y <= max.Y &&
             		  min.Z <= p.Z && p.Z <= max.Z );
		}
		
		public static BoundingBox Union( BoundingBox b1, BoundingBox b2 )
		{
			Point low = new Point( Math.Min( b1.min.X, b2.min.X ), 
			                       Math.Min( b1.min.Y, b2.min.Y ),
			                       Math.Min( b1.min.Z, b2.min.Z ) );
			
			Point high = new Point( Math.Max( b1.max.X, b2.max.X ), 
                        		    Math.Max( b1.max.Y, b2.max.Y ),
                       			    Math.Max( b1.max.Z, b2.max.Z ) );
			
			return new BoundingBox( low, high );
		}
		
		private static float VectorElement(Vector v, int i)
		{
			Debug.Assert( 0 <= i && i <= 2 );
			
			switch ( i )
			{
			case 0:
				return v.X;
			case 1:
				return v.Y;
			case 2:
				return v.Z;
			}
			
			return float.NaN;
		}
		
		private static float PointElement(Point v, int i)
		{
			Debug.Assert( 0 <= i && i <= 2 );
			
			switch ( i )
			{
			case 0:
				return v.X;
			case 1:
				return v.Y;
			case 2:
				return v.Z;
			}
			
			return float.NaN;
		}
		
		public bool Intersects( Ray r )
		{

			if ( IsInside(r.Origin) )
       			return true;

    		float t0 = 0.0f; float t1 = float.PositiveInfinity;

		    for ( int i = 0; i < 3; i++ ) {
		        float invRayDir = 1.0f / BoundingBox.VectorElement(r.Direction, i);
		        float tNear = ( BoundingBox.PointElement(min, i) - BoundingBox.PointElement(r.Origin, i) ) * invRayDir;
		        float tFar = ( BoundingBox.PointElement(max, i) - BoundingBox.PointElement(r.Origin, i) ) * invRayDir;
		
		        if ( tNear > tFar ) {
		            float temp = tNear;
		            tNear = tFar;
		            tFar = temp;
		        }
		
		        t0 = tNear > t0 ? tNear : t0;
		        t1 = tFar < t1 ? tFar : t1;
		
		        if ( t0 > t1 )
		            return false;
		    }
		
		    return true;
		}
		
		public static BoundingBox operator* ( Matrix m, BoundingBox b )
		{
			return new BoundingBox( m * b.min, m * b.max );	
		}
		
		public IList<BoundingBox> Octants()
		{
			Vector dx = new Vector( (max.X - min.X) * 0.5f, 0f, 0f );
			Vector dy = new Vector( 0f, (max.Y - min.Y) * 0.5f, 0f );
			Vector dz = new Vector( 0f, 0f, (max.Z - min.Z) * 0.5f );
			
			List<BoundingBox> ret = new List<BoundingBox>(8);
			
			ret.Add( new BoundingBox( min          , min + dx + dy + dz          ) );
			ret.Add( new BoundingBox( min + dx     , min + dx + dy + dz + dx     ) );
			ret.Add( new BoundingBox( min + dz     , min + dx + dy + dz + dz     ) );
			ret.Add( new BoundingBox( min + dz + dx, min + dx + dy + dz + dx + dz) );
			
			ret.Add( new BoundingBox( min + dy          , min + dx + dy + dy + dz) );
			ret.Add( new BoundingBox( min + dy + dx     , min + dx + dy + dy + dz + dx ) );
			ret.Add( new BoundingBox( min + dy + dz     , min + dx + dy + dy + dz + dz ) );
			ret.Add( new BoundingBox( min + dy + dx + dz, min + dx + dy + dy + dz + dx + dz) );
			
			return ret;
		}
		
		public static BoundingBox Intersection( BoundingBox b1, BoundingBox b2 )
		{
			if ( !b1.Overlaps( b2 ) )
			{
				return new BoundingBox( new Point( 0, 0, 0 ) );	
			}
			
			return new BoundingBox( new Point( Math.Max( b1.min.X, b2.min.X ),
			                                   Math.Max( b1.min.Y, b2.min.Y ),
			                                   Math.Max( b1.min.Z, b2.min.Z ) ),
			                       
			                       new Point( Math.Min( b1.max.X, b2.max.X ),
			                                  Math.Min( b1.max.Y, b2.max.Y ),
			                                  Math.Min( b1.max.Z, b2.max.Z ) ) );
		}
	}
}

